import {Elysia, t} from 'elysia'
import {eq, inArray, and} from 'drizzle-orm'
import {sysMenu, sysRoleMenu} from './schema/schema'
import os from 'os'
import {format} from 'date-fns'
import {ElysiaWS} from 'elysia/dist/ws'
import {checkReferer, envPlugin, formatErrorMsg, formatLog, formatPathForAuth, getIPPool, IPValidation} from './utils/dw'
import RedisClient from './utils/redis'
import db from './utils/db'
import jwt from '@elysiajs/jwt'
import {TokenInfo} from './types/auth'
import {ForbiddenError, getWSOutlineExcept, isAdmin, RESCODE, UnauthorizedError} from './utils/auth'
import {whiteUrl} from './utils/const'
import {rateLimit, type Generator} from 'elysia-rate-limit'
import common from './api'
import user from './api/user'
import loginLog from './api/login-log'
import menu from './api/menu'
import reqLog from './api/req-log'

const keyGenerator: Generator<{ip: string}> = async (req, server, {ip}) => Bun.hash(JSON.stringify(ip)).toString()

const env = t.Object({
  WHITELIST: t.String(),
  REDIS_HOST: t.String(),
  REDIS_PORT: t.String(),
  REDIS_PASSWORD: t.String(),
})

const getLocalIPs = () => {
  const interfaces = os.networkInterfaces()
  const ips = Object.keys(interfaces).reduce((result: string[], iface: string) => {
    ;(interfaces[iface] as os.NetworkInterfaceInfo[]).forEach(function (info) {
      if (info.family === 'IPv4' && !info.internal) {
        result.push(info.address)
      }
    })
    return result
  }, [])
  return ips
}

const start = (hostname: string, port: number) => {
  const ips = getLocalIPs()
  console.log(`\x1B[90m\x1B[1m\u279C\x1B[39m [${format(new Date(), 'yyyy-MM-dd HH:mm:ss')}] \x1B[90m\x1B[1m[Elysia]\x1B[0m 🦊 Server Is Running. \n`)
  console.log(`    \x1B[35m\u279C    Local:\x1B[39m    \x1B[34m\x1B[4mhttp://${hostname}:${port}\x1B[0m`)
  ips.forEach(e => {
    console.log(`    \x1B[35m\u279C  Network:\x1B[39m    \x1B[34m\x1B[4mhttp://${e}:${port}\x1B[0m`)
  })
}

const connectionMap = new Map<string, ElysiaWS<any>>()

const app = new Elysia()
  .decorate(() => {
    const _env = envPlugin(env)
    return {
      redis: new RedisClient({host: _env.REDIS_HOST, username: 'default', port: Number(_env.REDIS_PORT), password: _env.REDIS_PASSWORD}),
      env: _env,
      db,
    }
  })
  .use(
    jwt({
      name: 'jwt',
      secret: 'dwyanewang',
      schema: t.Object({
        sign: t.String(),
        isRefresh: t.Boolean(),
        roleIds: t.Array(t.Number()),
        userId: t.Number(),
        username: t.String(),
        timestamp: t.Number(),
      }),
    }),
  )
  .state({start: 0n, ip: '', timestamp: new Date(), error: false, curUser: undefined} as {
    start: bigint
    timestamp: Date
    ip: string
    error: boolean
    curUser?: TokenInfo
  })
  .error({
    UNAUTHORIZED: UnauthorizedError,
    FORBIDDEN: ForbiddenError,
  })
  .onRequest(({store, request, env}) => {
    store.start = process.hrtime.bigint()
    store.timestamp = new Date()
    store.ip = request.headers.get('x-real-ip') || IPValidation.generateIP(app.server?.requestIP(request)?.address ?? '')
    const whiteIPList = env.WHITELIST.split(',')
    store.error = false
    const _checkReferer = checkReferer(request, whiteIPList)
    if (_checkReferer) {
      store.error = true
      throw new ForbiddenError(_checkReferer)
    }
  })
  .onBeforeHandle(async ({headers, store, jwt, path, redis, db}) => {
    if (!['/api/login', '/api/refreshToken', '/api/upload', '/ws', '/api/test'].includes(path) && !path.startsWith('/api/file')) {
      const token = headers['authorization']
      store.curUser = (token && (await jwt.verify(token.substring(7)))) || undefined
      if (!store.curUser) throw new UnauthorizedError('非法Token，请联系管理员')
      if (store.curUser.isRefresh) throw new UnauthorizedError('登录失效，请重新登录')
      const sign = await redis.get(`user:sign:${store.curUser.userId}`)
      if (store.curUser.sign !== sign) throw new UnauthorizedError('登录失效或用户密码已更改，请重新登录')
      getWSOutlineExcept(headers['request-ws-status'])
      const _token = await redis.get(`user:token:${store.curUser.userId}`)
      if (token!.substring(7) !== _token) throw new UnauthorizedError('登录失效或无权限访问')
      let urlList: string[] | string | null = await redis.get(`user:auth:${store.curUser.userId}`)
      if (!urlList) {
        urlList = (
          await db
            .select({perms: sysMenu.perms})
            .from(sysRoleMenu)
            .leftJoin(sysMenu, eq(sysMenu.type, 2))
            .where(and(inArray(sysRoleMenu.roleId, store.curUser.roleIds), eq(sysRoleMenu.menuId, sysMenu.id)))
        ).map(e => e.perms) as string[]
        await redis.set(`user:auth:${store.curUser.userId}`, JSON.stringify(urlList))
      } else {
        urlList = JSON.parse(urlList) as string[]
      }
      const _curUrl = formatPathForAuth(path)
      if (!urlList.includes(_curUrl) && !_curUrl.endsWith('_page_list') && !whiteUrl.includes(_curUrl) && !(await isAdmin(db, store.curUser)))
        throw new ForbiddenError(`暂无${path.split('?')[0]}接口权限，请联系管理员`)
    }
  })
  .mapResponse(({response}) => {
    if (response instanceof Response) return response
    if (response instanceof Blob) return new Response(response)
    return new Response(
      Bun.gzipSync(
        new TextEncoder().encode(
          JSON.stringify({code: RESCODE.SUCCESS, data: typeof response === 'object' ? response : {}, msg: typeof response === 'string' ? response : 'ok'}),
        ),
      ),
      {
        headers: {
          'Content-Encoding': 'gzip',
          'Content-Type': 'application/json; charset=utf-8',
        },
      },
    )
  })
  .onError(({store, request, params, path, body, query, error, code, db, redis}) => {
    const errorMsg = formatErrorMsg(error.message, path)
    store.error = true
    formatLog({
      timestamp: store.timestamp,
      ip: store.ip,
      start: store.start,
      request,
      path,
      params,
      body,
      query,
      errorMsg,
      db,
      redis,
      curUser: store.curUser,
      code: RESCODE[code],
    })
    return new Response(Bun.gzipSync(new TextEncoder().encode(JSON.stringify({code: RESCODE[code], data: {}, msg: errorMsg}))), {
      headers: {
        'Content-Encoding': 'gzip',
        'Content-Type': 'application/json; charset=utf-8',
      },
      status: RESCODE[code],
    })
  })
  .onAfterResponse(async ({store: {timestamp, ip, start, error, curUser}, db, redis, request, params, path, body, query}) => {
    if (!error && path !== '/ws') {
      formatLog({timestamp, ip, start, request, path, params, body, query, db, redis, curUser})
    }
  })
  .ws('/ws', {
    message(ws, message) {
      if (!ws.data.store.curUser) {
        ws.close()
        return
      }
      if (message === 'ping') ws.send('pong')
      if (message === 'close') ws.close()
    },
    async open(ws) {
      console.log(
        `\x1B[90m\x1B[1m\u279C\x1B[39m [${format(new Date(), 'yyyy-MM-dd HH:mm:ss')}] \x1B[90m\x1B[1m[Websocket]\x1B[0m the ip \x1B[1m${
          ws.data.store.ip
        }\x1B[0m is\x1B[32m connected\x1B[0m.`,
      )
      const curUser: TokenInfo | undefined = (await ws.data.jwt.verify(ws.data.headers['sec-websocket-protocol'])) || undefined
      connectionMap.forEach(async _ws => {
        const _curUser: TokenInfo | undefined = (await ws.data.jwt.verify(_ws.data.headers['sec-websocket-protocol'])) || undefined
        // 判断header携带的用户ID一致但token不一致时，代表同一账号在另一设备上登录，则将前一登录用户强制登出
        if (_curUser?.userId === curUser?.userId) {
          if (_ws?.data.headers['sec-websocket-protocol'] !== ws.data.headers['sec-websocket-protocol']) {
            _ws!.send(JSON.stringify({type: 'outline', msg: '该账号已在其他设备上登录！'}))
          }
        }
      })
      connectionMap.set(ws.id, ws as unknown as ElysiaWS<any>)
      console.log(
        `\x1B[90m\x1B[1m\u279C\x1B[39m [${format(new Date(), 'yyyy-MM-dd HH:mm:ss')}] \x1B[90m\x1B[1m[Websocket]\x1B[0m the ip pool contains \x1B[1m${getIPPool(
          connectionMap,
        )}\x1B[0m.`,
      )
    },
    close(ws) {
      console.log(
        `\x1B[90m\x1B[1m\u279C\x1B[39m [${format(new Date(), 'yyyy-MM-dd HH:mm:ss')}] \x1B[90m\x1B[1m[Websocket]\x1B[0m the ip \x1B[1m${
          ws.data.store.ip
        }\x1B[0m is\x1B[33m disconnected\x1B[0m.`,
      )
      connectionMap.delete(ws.id)
      console.log(
        `\x1B[90m\x1B[1m\u279C\x1B[39m [${format(new Date(), 'yyyy-MM-dd HH:mm:ss')}] \x1B[90m\x1B[1m[Websocket]\x1B[0m the ip pool ${
          connectionMap.size ? 'contains \x1B[1m' + getIPPool(connectionMap) + '\x1B[0m.' : 'is empty.'
        }`,
      )
    },
  })
  .use(
    rateLimit({
      duration: 10000, // 10秒内最大6个请求
      max: 6,
      generator: keyGenerator,
      errorResponse: new Error('Rate-Limit Reached'),
    }),
  )

// 路由表
app.group('/api', _app =>
  _app
    .use(common)
    .group('/user', __app => __app.use(user))
    .group('/login-log', __app => __app.use(loginLog))
    .group('/menu', __app => __app.use(menu))
    .group('/req-log', __app => __app.use(reqLog)),
)

app.listen(process.env.SERVER_PORT as string, ({hostname, port}) => {
  start(hostname, port)
})

export type ElysiaApp = typeof app
